import {
	AnimationClip,
	Bone,
	Box3,
	BufferAttribute,
	BufferGeometry,
	ClampToEdgeWrapping,
	Color,
	ColorManagement,
	DirectionalLight,
	DoubleSide,
	FileLoader,
	FrontSide,
	Group,
	ImageBitmapLoader,
	InstancedMesh,
	InterleavedBuffer,
	InterleavedBufferAttribute,
	Interpolant,
	InterpolateDiscrete,
	InterpolateLinear,
	Line,
	LineBasicMaterial,
	LineLoop,
	LineSegments,
	LinearFilter,
	LinearMipmapLinearFilter,
	LinearMipmapNearestFilter,
	LinearSRGBColorSpace,
	Loader,
	LoaderUtils,
	Material,
	MathUtils,
	Matrix4,
	Mesh,
	MeshBasicMaterial,
	MeshPhysicalMaterial,
	MeshStandardMaterial,
	MirroredRepeatWrapping,
	NearestFilter,
	NearestMipmapLinearFilter,
	NearestMipmapNearestFilter,
	NumberKeyframeTrack,
	Object3D,
	OrthographicCamera,
	PerspectiveCamera,
	PointLight,
	Points,
	PointsMaterial,
	PropertyBinding,
	Quaternion,
	QuaternionKeyframeTrack,
	RepeatWrapping,
	Skeleton,
	SkinnedMesh,
	Sphere,
	SpotLight,
	Texture,
	TextureLoader,
	TriangleFanDrawMode,
	TriangleStripDrawMode,
	Vector2,
	Vector3,
	VectorKeyframeTrack,
	SRGBColorSpace,
	InstancedBufferAttribute
} from '../three.module.min.js';
import { toTrianglesDrawMode } from '../utils/BufferGeometryUtils.js';

class GLTFLoader extends Loader {

	constructor(manager) {

		super(manager);

		this.dracoLoader = null;
		this.ktx2Loader = null;
		this.meshoptDecoder = null;

		this.pluginCallbacks = [];

		this.register(function (parser) {

			return new GLTFMaterialsClearcoatExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsDispersionExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFTextureBasisUExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFTextureWebPExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFTextureAVIFExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsSheenExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsTransmissionExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsVolumeExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsIorExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsEmissiveStrengthExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsSpecularExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsIridescenceExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsAnisotropyExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMaterialsBumpExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFLightsExtension(parser);

		});

		this.register(function (parser) {

			return new GLTFMeshoptCompression(parser);

		});

		this.register(function (parser) {

			return new GLTFMeshGpuInstancing(parser);

		});

	}

	load(url, onLoad, onProgress, onError) {

		const scope = this;

		let resourcePath;

		if (this.resourcePath !== '') {

			resourcePath = this.resourcePath;

		} else if (this.path !== '') {

			// If a base path is set, resources will be relative paths from that plus the relative path of the gltf file
			// Example  path = 'https://my-cnd-server.com/', url = 'assets/models/model.gltf'
			// resourcePath = 'https://my-cnd-server.com/assets/models/'
			// referenced resource 'model.bin' will be loaded from 'https://my-cnd-server.com/assets/models/model.bin'
			// referenced resource '../textures/texture.png' will be loaded from 'https://my-cnd-server.com/assets/textures/texture.png'
			const relativeUrl = LoaderUtils.extractUrlBase(url);
			resourcePath = LoaderUtils.resolveURL(relativeUrl, this.path);

		} else {

			resourcePath = LoaderUtils.extractUrlBase(url);

		}

		// Tells the LoadingManager to track an extra item, which resolves after
		// the model is fully loaded. This means the count of items loaded will
		// be incorrect, but ensures manager.onLoad() does not fire early.
		this.manager.itemStart(url);

		const _onError = function (e) {

			if (onError) {

				onError(e);

			} else {

				console.error(e);

			}

			scope.manager.itemError(url);
			scope.manager.itemEnd(url);

		};

		const loader = new FileLoader(this.manager);

		loader.setPath(this.path);
		loader.setResponseType('arraybuffer');
		loader.setRequestHeader(this.requestHeader);
		loader.setWithCredentials(this.withCredentials);

		loader.load(url, function (data) {

			try {

				scope.parse(data, resourcePath, function (gltf) {

					onLoad(gltf);

					scope.manager.itemEnd(url);

				}, _onError);

			} catch (e) {

				_onError(e);

			}

		}, onProgress, _onError);

	}

	setDRACOLoader(dracoLoader) {

		this.dracoLoader = dracoLoader;
		return this;

	}

	setKTX2Loader(ktx2Loader) {

		this.ktx2Loader = ktx2Loader;
		return this;

	}

	setMeshoptDecoder(meshoptDecoder) {

		this.meshoptDecoder = meshoptDecoder;
		return this;

	}

	register(callback) {

		if (this.pluginCallbacks.indexOf(callback) === - 1) {

			this.pluginCallbacks.push(callback);

		}

		return this;

	}

	unregister(callback) {

		if (this.pluginCallbacks.indexOf(callback) !== - 1) {

			this.pluginCallbacks.splice(this.pluginCallbacks.indexOf(callback), 1);

		}

		return this;

	}

	parse(data, path, onLoad, onError) {

		let json;
		const extensions = {};
		const plugins = {};
		const textDecoder = new TextDecoder();

		if (typeof data === 'string') {

			json = JSON.parse(data);

		} else if (data instanceof ArrayBuffer) {

			const magic = textDecoder.decode(new Uint8Array(data, 0, 4));

			if (magic === BINARY_EXTENSION_HEADER_MAGIC) {

				try {

					extensions[EXTENSIONS.KHR_BINARY_GLTF] = new GLTFBinaryExtension(data);

				} catch (error) {

					if (onError) onError(error);
					return;

				}

				json = JSON.parse(extensions[EXTENSIONS.KHR_BINARY_GLTF].content);

			} else {

				json = JSON.parse(textDecoder.decode(data));

			}

		} else {

			json = data;

		}

		if (json.asset === undefined || json.asset.version[0] < 2) {

			if (onError) onError(new Error('THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.'));
			return;

		}

		const parser = new GLTFParser(json, {

			path: path || this.resourcePath || '',
			crossOrigin: this.crossOrigin,
			requestHeader: this.requestHeader,
			manager: this.manager,
			ktx2Loader: this.ktx2Loader,
			meshoptDecoder: this.meshoptDecoder

		});

		parser.fileLoader.setRequestHeader(this.requestHeader);

		for (let i = 0; i < this.pluginCallbacks.length; i++) {

			const plugin = this.pluginCallbacks[i](parser);

			if (!plugin.name) console.error('THREE.GLTFLoader: Invalid plugin found: missing name');

			plugins[plugin.name] = plugin;

			// Workaround to avoid determining as unknown extension
			// in addUnknownExtensionsToUserData().
			// Remove this workaround if we move all the existing
			// extension handlers to plugin system
			extensions[plugin.name] = true;

		}

		if (json.extensionsUsed) {

			for (let i = 0; i < json.extensionsUsed.length; ++i) {

				const extensionName = json.extensionsUsed[i];
				const extensionsRequired = json.extensionsRequired || [];

				switch (extensionName) {

					case EXTENSIONS.KHR_MATERIALS_UNLIT:
						extensions[extensionName] = new GLTFMaterialsUnlitExtension();
						break;

					case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION:
						extensions[extensionName] = new GLTFDracoMeshCompressionExtension(json, this.dracoLoader);
						break;

					case EXTENSIONS.KHR_TEXTURE_TRANSFORM:
						extensions[extensionName] = new GLTFTextureTransformExtension();
						break;

					case EXTENSIONS.KHR_MESH_QUANTIZATION:
						extensions[extensionName] = new GLTFMeshQuantizationExtension();
						break;

					default:

						if (extensionsRequired.indexOf(extensionName) >= 0 && plugins[extensionName] === undefined) {

							console.warn('THREE.GLTFLoader: Unknown extension "' + extensionName + '".');

						}

				}

			}

		}

		parser.setExtensions(extensions);
		parser.setPlugins(plugins);
		parser.parse(onLoad, onError);

	}

	parseAsync(data, path) {

		const scope = this;

		return new Promise(function (resolve, reject) {

			scope.parse(data, path, resolve, reject);

		});

	}

}

/* GLTFREGISTRY */

function GLTFRegistry() {

	let objects = {};

	return {

		get: function (key) {

			return objects[key];

		},

		add: function (key, object) {

			objects[key] = object;

		},

		remove: function (key) {

			delete objects[key];

		},

		removeAll: function () {

			objects = {};

		}

	};

}

/*********************************/
/********** EXTENSIONS ***********/
/*********************************/

const EXTENSIONS = {
	KHR_BINARY_GLTF: 'KHR_binary_glTF',
	KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression',
	KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual',
	KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat',
	KHR_MATERIALS_DISPERSION: 'KHR_materials_dispersion',
	KHR_MATERIALS_IOR: 'KHR_materials_ior',
	KHR_MATERIALS_SHEEN: 'KHR_materials_sheen',
	KHR_MATERIALS_SPECULAR: 'KHR_materials_specular',
	KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission',
	KHR_MATERIALS_IRIDESCENCE: 'KHR_materials_iridescence',
	KHR_MATERIALS_ANISOTROPY: 'KHR_materials_anisotropy',
	KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
	KHR_MATERIALS_VOLUME: 'KHR_materials_volume',
	KHR_TEXTURE_BASISU: 'KHR_texture_basisu',
	KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform',
	KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization',
	KHR_MATERIALS_EMISSIVE_STRENGTH: 'KHR_materials_emissive_strength',
	EXT_MATERIALS_BUMP: 'EXT_materials_bump',
	EXT_TEXTURE_WEBP: 'EXT_texture_webp',
	EXT_TEXTURE_AVIF: 'EXT_texture_avif',
	EXT_MESHOPT_COMPRESSION: 'EXT_meshopt_compression',
	EXT_MESH_GPU_INSTANCING: 'EXT_mesh_gpu_instancing'
};

/**
 * Punctual Lights Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
 */
class GLTFLightsExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL;

		// Object3D instance caches
		this.cache = { refs: {}, uses: {} };

	}

	_markDefs() {

		const parser = this.parser;
		const nodeDefs = this.parser.json.nodes || [];

		for (let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) {

			const nodeDef = nodeDefs[nodeIndex];

			if (nodeDef.extensions
				&& nodeDef.extensions[this.name]
				&& nodeDef.extensions[this.name].light !== undefined) {

				parser._addNodeRef(this.cache, nodeDef.extensions[this.name].light);

			}

		}

	}

	_loadLight(lightIndex) {

		const parser = this.parser;
		const cacheKey = 'light:' + lightIndex;
		let dependency = parser.cache.get(cacheKey);

		if (dependency) return dependency;

		const json = parser.json;
		const extensions = (json.extensions && json.extensions[this.name]) || {};
		const lightDefs = extensions.lights || [];
		const lightDef = lightDefs[lightIndex];
		let lightNode;

		const color = new Color(0xffffff);

		if (lightDef.color !== undefined) color.setRGB(lightDef.color[0], lightDef.color[1], lightDef.color[2], LinearSRGBColorSpace);

		const range = lightDef.range !== undefined ? lightDef.range : 0;

		switch (lightDef.type) {

			case 'directional':
				lightNode = new DirectionalLight(color);
				lightNode.target.position.set(0, 0, - 1);
				lightNode.add(lightNode.target);
				break;

			case 'point':
				lightNode = new PointLight(color);
				lightNode.distance = range;
				break;

			case 'spot':
				lightNode = new SpotLight(color);
				lightNode.distance = range;
				// Handle spotlight properties.
				lightDef.spot = lightDef.spot || {};
				lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0;
				lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0;
				lightNode.angle = lightDef.spot.outerConeAngle;
				lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle;
				lightNode.target.position.set(0, 0, - 1);
				lightNode.add(lightNode.target);
				break;

			default:
				throw new Error('THREE.GLTFLoader: Unexpected light type: ' + lightDef.type);

		}

		// Some lights (e.g. spot) default to a position other than the origin. Reset the position
		// here, because node-level parsing will only override position if explicitly specified.
		lightNode.position.set(0, 0, 0);

		lightNode.decay = 2;

		assignExtrasToUserData(lightNode, lightDef);

		if (lightDef.intensity !== undefined) lightNode.intensity = lightDef.intensity;

		lightNode.name = parser.createUniqueName(lightDef.name || ('light_' + lightIndex));

		dependency = Promise.resolve(lightNode);

		parser.cache.add(cacheKey, dependency);

		return dependency;

	}

	getDependency(type, index) {

		if (type !== 'light') return;

		return this._loadLight(index);

	}

	createNodeAttachment(nodeIndex) {

		const self = this;
		const parser = this.parser;
		const json = parser.json;
		const nodeDef = json.nodes[nodeIndex];
		const lightDef = (nodeDef.extensions && nodeDef.extensions[this.name]) || {};
		const lightIndex = lightDef.light;

		if (lightIndex === undefined) return null;

		return this._loadLight(lightIndex).then(function (light) {

			return parser._getNodeRef(self.cache, lightIndex, light);

		});

	}

}

/**
 * Unlit Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit
 */
class GLTFMaterialsUnlitExtension {

	constructor() {

		this.name = EXTENSIONS.KHR_MATERIALS_UNLIT;

	}

	getMaterialType() {

		return MeshBasicMaterial;

	}

	extendParams(materialParams, materialDef, parser) {

		const pending = [];

		materialParams.color = new Color(1.0, 1.0, 1.0);
		materialParams.opacity = 1.0;

		const metallicRoughness = materialDef.pbrMetallicRoughness;

		if (metallicRoughness) {

			if (Array.isArray(metallicRoughness.baseColorFactor)) {

				const array = metallicRoughness.baseColorFactor;

				materialParams.color.setRGB(array[0], array[1], array[2], LinearSRGBColorSpace);
				materialParams.opacity = array[3];

			}

			if (metallicRoughness.baseColorTexture !== undefined) {

				pending.push(parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture, SRGBColorSpace));

			}

		}

		return Promise.all(pending);

	}

}

/**
 * Materials Emissive Strength Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/blob/5768b3ce0ef32bc39cdf1bef10b948586635ead3/extensions/2.0/Khronos/KHR_materials_emissive_strength/README.md
 */
class GLTFMaterialsEmissiveStrengthExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_EMISSIVE_STRENGTH;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const emissiveStrength = materialDef.extensions[this.name].emissiveStrength;

		if (emissiveStrength !== undefined) {

			materialParams.emissiveIntensity = emissiveStrength;

		}

		return Promise.resolve();

	}

}

/**
 * Clearcoat Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat
 */
class GLTFMaterialsClearcoatExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const pending = [];

		const extension = materialDef.extensions[this.name];

		if (extension.clearcoatFactor !== undefined) {

			materialParams.clearcoat = extension.clearcoatFactor;

		}

		if (extension.clearcoatTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'clearcoatMap', extension.clearcoatTexture));

		}

		if (extension.clearcoatRoughnessFactor !== undefined) {

			materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor;

		}

		if (extension.clearcoatRoughnessTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture));

		}

		if (extension.clearcoatNormalTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture));

			if (extension.clearcoatNormalTexture.scale !== undefined) {

				const scale = extension.clearcoatNormalTexture.scale;

				materialParams.clearcoatNormalScale = new Vector2(scale, scale);

			}

		}

		return Promise.all(pending);

	}

}

/**
 * Materials dispersion Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_dispersion
 */
class GLTFMaterialsDispersionExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_DISPERSION;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const extension = materialDef.extensions[this.name];

		materialParams.dispersion = extension.dispersion !== undefined ? extension.dispersion : 0;

		return Promise.resolve();

	}

}

/**
 * Iridescence Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence
 */
class GLTFMaterialsIridescenceExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_IRIDESCENCE;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const pending = [];

		const extension = materialDef.extensions[this.name];

		if (extension.iridescenceFactor !== undefined) {

			materialParams.iridescence = extension.iridescenceFactor;

		}

		if (extension.iridescenceTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'iridescenceMap', extension.iridescenceTexture));

		}

		if (extension.iridescenceIor !== undefined) {

			materialParams.iridescenceIOR = extension.iridescenceIor;

		}

		if (materialParams.iridescenceThicknessRange === undefined) {

			materialParams.iridescenceThicknessRange = [100, 400];

		}

		if (extension.iridescenceThicknessMinimum !== undefined) {

			materialParams.iridescenceThicknessRange[0] = extension.iridescenceThicknessMinimum;

		}

		if (extension.iridescenceThicknessMaximum !== undefined) {

			materialParams.iridescenceThicknessRange[1] = extension.iridescenceThicknessMaximum;

		}

		if (extension.iridescenceThicknessTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'iridescenceThicknessMap', extension.iridescenceThicknessTexture));

		}

		return Promise.all(pending);

	}

}

/**
 * Sheen Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen
 */
class GLTFMaterialsSheenExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_SHEEN;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const pending = [];

		materialParams.sheenColor = new Color(0, 0, 0);
		materialParams.sheenRoughness = 0;
		materialParams.sheen = 1;

		const extension = materialDef.extensions[this.name];

		if (extension.sheenColorFactor !== undefined) {

			const colorFactor = extension.sheenColorFactor;
			materialParams.sheenColor.setRGB(colorFactor[0], colorFactor[1], colorFactor[2], LinearSRGBColorSpace);

		}

		if (extension.sheenRoughnessFactor !== undefined) {

			materialParams.sheenRoughness = extension.sheenRoughnessFactor;

		}

		if (extension.sheenColorTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'sheenColorMap', extension.sheenColorTexture, SRGBColorSpace));

		}

		if (extension.sheenRoughnessTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'sheenRoughnessMap', extension.sheenRoughnessTexture));

		}

		return Promise.all(pending);

	}

}

/**
 * Transmission Materials Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission
 * Draft: https://github.com/KhronosGroup/glTF/pull/1698
 */
class GLTFMaterialsTransmissionExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const pending = [];

		const extension = materialDef.extensions[this.name];

		if (extension.transmissionFactor !== undefined) {

			materialParams.transmission = extension.transmissionFactor;

		}

		if (extension.transmissionTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'transmissionMap', extension.transmissionTexture));

		}

		return Promise.all(pending);

	}

}

/**
 * Materials Volume Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume
 */
class GLTFMaterialsVolumeExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_VOLUME;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const pending = [];

		const extension = materialDef.extensions[this.name];

		materialParams.thickness = extension.thicknessFactor !== undefined ? extension.thicknessFactor : 0;

		if (extension.thicknessTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'thicknessMap', extension.thicknessTexture));

		}

		materialParams.attenuationDistance = extension.attenuationDistance || Infinity;

		const colorArray = extension.attenuationColor || [1, 1, 1];
		materialParams.attenuationColor = new Color().setRGB(colorArray[0], colorArray[1], colorArray[2], LinearSRGBColorSpace);

		return Promise.all(pending);

	}

}

/**
 * Materials ior Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_ior
 */
class GLTFMaterialsIorExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_IOR;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const extension = materialDef.extensions[this.name];

		materialParams.ior = extension.ior !== undefined ? extension.ior : 1.5;

		return Promise.resolve();

	}

}

/**
 * Materials specular Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_specular
 */
class GLTFMaterialsSpecularExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_SPECULAR;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const pending = [];

		const extension = materialDef.extensions[this.name];

		materialParams.specularIntensity = extension.specularFactor !== undefined ? extension.specularFactor : 1.0;

		if (extension.specularTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'specularIntensityMap', extension.specularTexture));

		}

		const colorArray = extension.specularColorFactor || [1, 1, 1];
		materialParams.specularColor = new Color().setRGB(colorArray[0], colorArray[1], colorArray[2], LinearSRGBColorSpace);

		if (extension.specularColorTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'specularColorMap', extension.specularColorTexture, SRGBColorSpace));

		}

		return Promise.all(pending);

	}

}


/**
 * Materials bump Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/EXT_materials_bump
 */
class GLTFMaterialsBumpExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.EXT_MATERIALS_BUMP;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const pending = [];

		const extension = materialDef.extensions[this.name];

		materialParams.bumpScale = extension.bumpFactor !== undefined ? extension.bumpFactor : 1.0;

		if (extension.bumpTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'bumpMap', extension.bumpTexture));

		}

		return Promise.all(pending);

	}

}

/**
 * Materials anisotropy Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_anisotropy
 */
class GLTFMaterialsAnisotropyExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_MATERIALS_ANISOTROPY;

	}

	getMaterialType(materialIndex) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) return null;

		return MeshPhysicalMaterial;

	}

	extendMaterialParams(materialIndex, materialParams) {

		const parser = this.parser;
		const materialDef = parser.json.materials[materialIndex];

		if (!materialDef.extensions || !materialDef.extensions[this.name]) {

			return Promise.resolve();

		}

		const pending = [];

		const extension = materialDef.extensions[this.name];

		if (extension.anisotropyStrength !== undefined) {

			materialParams.anisotropy = extension.anisotropyStrength;

		}

		if (extension.anisotropyRotation !== undefined) {

			materialParams.anisotropyRotation = extension.anisotropyRotation;

		}

		if (extension.anisotropyTexture !== undefined) {

			pending.push(parser.assignTexture(materialParams, 'anisotropyMap', extension.anisotropyTexture));

		}

		return Promise.all(pending);

	}

}

/**
 * BasisU Texture Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu
 */
class GLTFTextureBasisUExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.KHR_TEXTURE_BASISU;

	}

	loadTexture(textureIndex) {

		const parser = this.parser;
		const json = parser.json;

		const textureDef = json.textures[textureIndex];

		if (!textureDef.extensions || !textureDef.extensions[this.name]) {

			return null;

		}

		const extension = textureDef.extensions[this.name];
		const loader = parser.options.ktx2Loader;

		if (!loader) {

			if (json.extensionsRequired && json.extensionsRequired.indexOf(this.name) >= 0) {

				throw new Error('THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures');

			} else {

				// Assumes that the extension is optional and that a fallback texture is present
				return null;

			}

		}

		return parser.loadTextureImage(textureIndex, extension.source, loader);

	}

}

/**
 * WebP Texture Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp
 */
class GLTFTextureWebPExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.EXT_TEXTURE_WEBP;
		this.isSupported = null;

	}

	loadTexture(textureIndex) {

		const name = this.name;
		const parser = this.parser;
		const json = parser.json;

		const textureDef = json.textures[textureIndex];

		if (!textureDef.extensions || !textureDef.extensions[name]) {

			return null;

		}

		const extension = textureDef.extensions[name];
		const source = json.images[extension.source];

		let loader = parser.textureLoader;
		if (source.uri) {

			const handler = parser.options.manager.getHandler(source.uri);
			if (handler !== null) loader = handler;

		}

		return this.detectSupport().then(function (isSupported) {

			if (isSupported) return parser.loadTextureImage(textureIndex, extension.source, loader);

			if (json.extensionsRequired && json.extensionsRequired.indexOf(name) >= 0) {

				throw new Error('THREE.GLTFLoader: WebP required by asset but unsupported.');

			}

			// Fall back to PNG or JPEG.
			return parser.loadTexture(textureIndex);

		});

	}

	detectSupport() {

		if (!this.isSupported) {

			this.isSupported = new Promise(function (resolve) {

				const image = new Image();

				// Lossy test image. Support for lossy images doesn't guarantee support for all
				// WebP images, unfortunately.
				image.src = '';

				image.onload = image.onerror = function () {

					resolve(image.height === 1);

				};

			});

		}

		return this.isSupported;

	}

}

/**
 * AVIF Texture Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_avif
 */
class GLTFTextureAVIFExtension {

	constructor(parser) {

		this.parser = parser;
		this.name = EXTENSIONS.EXT_TEXTURE_AVIF;
		this.isSupported = null;

	}

	loadTexture(textureIndex) {

		const name = this.name;
		const parser = this.parser;
		const json = parser.json;

		const textureDef = json.textures[textureIndex];

		if (!textureDef.extensions || !textureDef.extensions[name]) {

			return null;

		}

		const extension = textureDef.extensions[name];
		const source = json.images[extension.source];

		let loader = parser.textureLoader;
		if (source.uri) {

			const handler = parser.options.manager.getHandler(source.uri);
			if (handler !== null) loader = handler;

		}

		return this.detectSupport().then(function (isSupported) {

			if (isSupported) return parser.loadTextureImage(textureIndex, extension.source, loader);

			if (json.extensionsRequired && json.extensionsRequired.indexOf(name) >= 0) {

				throw new Error('THREE.GLTFLoader: AVIF required by asset but unsupported.');

			}

			// Fall back to PNG or JPEG.
			return parser.loadTexture(textureIndex);

		});

	}

	detectSupport() {

		if (!this.isSupported) {

			this.isSupported = new Promise(function (resolve) {

				const image = new Image();

				// Lossy test image.
				image.src = '';
				image.onload = image.onerror = function () {

					resolve(image.height === 1);

				};

			});

		}

		return this.isSupported;

	}

}

/**
 * meshopt BufferView Compression Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression
 */
class GLTFMeshoptCompression {

	constructor(parser) {

		this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION;
		this.parser = parser;

	}

	loadBufferView(index) {

		const json = this.parser.json;
		const bufferView = json.bufferViews[index];

		if (bufferView.extensions && bufferView.extensions[this.name]) {

			const extensionDef = bufferView.extensions[this.name];

			const buffer = this.parser.getDependency('buffer', extensionDef.buffer);
			const decoder = this.parser.options.meshoptDecoder;

			if (!decoder || !decoder.supported) {

				if (json.extensionsRequired && json.extensionsRequired.indexOf(this.name) >= 0) {

					throw new Error('THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files');

				} else {

					// Assumes that the extension is optional and that fallback buffer data is present
					return null;

				}

			}

			return buffer.then(function (res) {

				const byteOffset = extensionDef.byteOffset || 0;
				const byteLength = extensionDef.byteLength || 0;

				const count = extensionDef.count;
				const stride = extensionDef.byteStride;

				const source = new Uint8Array(res, byteOffset, byteLength);

				if (decoder.decodeGltfBufferAsync) {

					return decoder.decodeGltfBufferAsync(count, stride, source, extensionDef.mode, extensionDef.filter).then(function (res) {

						return res.buffer;

					});

				} else {

					// Support for MeshoptDecoder 0.18 or earlier, without decodeGltfBufferAsync
					return decoder.ready.then(function () {

						const result = new ArrayBuffer(count * stride);
						decoder.decodeGltfBuffer(new Uint8Array(result), count, stride, source, extensionDef.mode, extensionDef.filter);
						return result;

					});

				}

			});

		} else {

			return null;

		}

	}

}

/**
 * GPU Instancing Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_gpu_instancing
 *
 */
class GLTFMeshGpuInstancing {

	constructor(parser) {

		this.name = EXTENSIONS.EXT_MESH_GPU_INSTANCING;
		this.parser = parser;

	}

	createNodeMesh(nodeIndex) {

		const json = this.parser.json;
		const nodeDef = json.nodes[nodeIndex];

		if (!nodeDef.extensions || !nodeDef.extensions[this.name] ||
			nodeDef.mesh === undefined) {

			return null;

		}

		const meshDef = json.meshes[nodeDef.mesh];

		// No Points or Lines + Instancing support yet

		for (const primitive of meshDef.primitives) {

			if (primitive.mode !== WEBGL_CONSTANTS.TRIANGLES &&
				primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_STRIP &&
				primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_FAN &&
				primitive.mode !== undefined) {

				return null;

			}

		}

		const extensionDef = nodeDef.extensions[this.name];
		const attributesDef = extensionDef.attributes;

		// @TODO: Can we support InstancedMesh + SkinnedMesh?

		const pending = [];
		const attributes = {};

		for (const key in attributesDef) {

			pending.push(this.parser.getDependency('accessor', attributesDef[key]).then(accessor => {

				attributes[key] = accessor;
				return attributes[key];

			}));

		}

		if (pending.length < 1) {

			return null;

		}

		pending.push(this.parser.createNodeMesh(nodeIndex));

		return Promise.all(pending).then(results => {

			const nodeObject = results.pop();
			const meshes = nodeObject.isGroup ? nodeObject.children : [nodeObject];
			const count = results[0].count; // All attribute counts should be same
			const instancedMeshes = [];

			for (const mesh of meshes) {

				// Temporal variables
				const m = new Matrix4();
				const p = new Vector3();
				const q = new Quaternion();
				const s = new Vector3(1, 1, 1);

				const instancedMesh = new InstancedMesh(mesh.geometry, mesh.material, count);

				for (let i = 0; i < count; i++) {

					if (attributes.TRANSLATION) {

						p.fromBufferAttribute(attributes.TRANSLATION, i);

					}

					if (attributes.ROTATION) {

						q.fromBufferAttribute(attributes.ROTATION, i);

					}

					if (attributes.SCALE) {

						s.fromBufferAttribute(attributes.SCALE, i);

					}

					instancedMesh.setMatrixAt(i, m.compose(p, q, s));

				}

				// Add instance attributes to the geometry, excluding TRS.
				for (const attributeName in attributes) {

					if (attributeName === '_COLOR_0') {

						const attr = attributes[attributeName];
						instancedMesh.instanceColor = new InstancedBufferAttribute(attr.array, attr.itemSize, attr.normalized);

					} else if (attributeName !== 'TRANSLATION' &&
						attributeName !== 'ROTATION' &&
						attributeName !== 'SCALE') {

						mesh.geometry.setAttribute(attributeName, attributes[attributeName]);

					}

				}

				// Just in case
				Object3D.prototype.copy.call(instancedMesh, mesh);

				this.parser.assignFinalMaterial(instancedMesh);

				instancedMeshes.push(instancedMesh);

			}

			if (nodeObject.isGroup) {

				nodeObject.clear();

				nodeObject.add(...instancedMeshes);

				return nodeObject;

			}

			return instancedMeshes[0];

		});

	}

}

/* BINARY EXTENSION */
const BINARY_EXTENSION_HEADER_MAGIC = 'glTF';
const BINARY_EXTENSION_HEADER_LENGTH = 12;
const BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 };

class GLTFBinaryExtension {

	constructor(data) {

		this.name = EXTENSIONS.KHR_BINARY_GLTF;
		this.content = null;
		this.body = null;

		const headerView = new DataView(data, 0, BINARY_EXTENSION_HEADER_LENGTH);
		const textDecoder = new TextDecoder();

		this.header = {
			magic: textDecoder.decode(new Uint8Array(data.slice(0, 4))),
			version: headerView.getUint32(4, true),
			length: headerView.getUint32(8, true)
		};

		if (this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC) {

			throw new Error('THREE.GLTFLoader: Unsupported glTF-Binary header.');

		} else if (this.header.version < 2.0) {

			throw new Error('THREE.GLTFLoader: Legacy binary file detected.');

		}

		const chunkContentsLength = this.header.length - BINARY_EXTENSION_HEADER_LENGTH;
		const chunkView = new DataView(data, BINARY_EXTENSION_HEADER_LENGTH);
		let chunkIndex = 0;

		while (chunkIndex < chunkContentsLength) {

			const chunkLength = chunkView.getUint32(chunkIndex, true);
			chunkIndex += 4;

			const chunkType = chunkView.getUint32(chunkIndex, true);
			chunkIndex += 4;

			if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON) {

				const contentArray = new Uint8Array(data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength);
				this.content = textDecoder.decode(contentArray);

			} else if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN) {

				const byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex;
				this.body = data.slice(byteOffset, byteOffset + chunkLength);

			}

			// Clients must ignore chunks with unknown types.

			chunkIndex += chunkLength;

		}

		if (this.content === null) {

			throw new Error('THREE.GLTFLoader: JSON content not found.');

		}

	}

}

/**
 * DRACO Mesh Compression Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression
 */
class GLTFDracoMeshCompressionExtension {

	constructor(json, dracoLoader) {

		if (!dracoLoader) {

			throw new Error('THREE.GLTFLoader: No DRACOLoader instance provided.');

		}

		this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION;
		this.json = json;
		this.dracoLoader = dracoLoader;
		this.dracoLoader.preload();

	}

	decodePrimitive(primitive, parser) {

		const json = this.json;
		const dracoLoader = this.dracoLoader;
		const bufferViewIndex = primitive.extensions[this.name].bufferView;
		const gltfAttributeMap = primitive.extensions[this.name].attributes;
		const threeAttributeMap = {};
		const attributeNormalizedMap = {};
		const attributeTypeMap = {};

		for (const attributeName in gltfAttributeMap) {

			const threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase();

			threeAttributeMap[threeAttributeName] = gltfAttributeMap[attributeName];

		}

		for (const attributeName in primitive.attributes) {

			const threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase();

			if (gltfAttributeMap[attributeName] !== undefined) {

				const accessorDef = json.accessors[primitive.attributes[attributeName]];
				const componentType = WEBGL_COMPONENT_TYPES[accessorDef.componentType];

				attributeTypeMap[threeAttributeName] = componentType.name;
				attributeNormalizedMap[threeAttributeName] = accessorDef.normalized === true;

			}

		}

		return parser.getDependency('bufferView', bufferViewIndex).then(function (bufferView) {

			return new Promise(function (resolve, reject) {

				dracoLoader.decodeDracoFile(bufferView, function (geometry) {

					for (const attributeName in geometry.attributes) {

						const attribute = geometry.attributes[attributeName];
						const normalized = attributeNormalizedMap[attributeName];

						if (normalized !== undefined) attribute.normalized = normalized;

					}

					resolve(geometry);

				}, threeAttributeMap, attributeTypeMap, LinearSRGBColorSpace, reject);

			});

		});

	}

}

/**
 * Texture Transform Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform
 */
class GLTFTextureTransformExtension {

	constructor() {

		this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM;

	}

	extendTexture(texture, transform) {

		if ((transform.texCoord === undefined || transform.texCoord === texture.channel)
			&& transform.offset === undefined
			&& transform.rotation === undefined
			&& transform.scale === undefined) {

			// See https://github.com/mrdoob/three.js/issues/21819.
			return texture;

		}

		texture = texture.clone();

		if (transform.texCoord !== undefined) {

			texture.channel = transform.texCoord;

		}

		if (transform.offset !== undefined) {

			texture.offset.fromArray(transform.offset);

		}

		if (transform.rotation !== undefined) {

			texture.rotation = transform.rotation;

		}

		if (transform.scale !== undefined) {

			texture.repeat.fromArray(transform.scale);

		}

		texture.needsUpdate = true;

		return texture;

	}

}

/**
 * Mesh Quantization Extension
 *
 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization
 */
class GLTFMeshQuantizationExtension {

	constructor() {

		this.name = EXTENSIONS.KHR_MESH_QUANTIZATION;

	}

}

/*********************************/
/********** INTERPOLATION ********/
/*********************************/

// Spline Interpolation
// Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation
class GLTFCubicSplineInterpolant extends Interpolant {

	constructor(parameterPositions, sampleValues, sampleSize, resultBuffer) {

		super(parameterPositions, sampleValues, sampleSize, resultBuffer);

	}

	copySampleValue_(index) {

		// Copies a sample value to the result buffer. See description of glTF
		// CUBICSPLINE values layout in interpolate_() function below.

		const result = this.resultBuffer,
			values = this.sampleValues,
			valueSize = this.valueSize,
			offset = index * valueSize * 3 + valueSize;

		for (let i = 0; i !== valueSize; i++) {

			result[i] = values[offset + i];

		}

		return result;

	}

	interpolate_(i1, t0, t, t1) {

		const result = this.resultBuffer;
		const values = this.sampleValues;
		const stride = this.valueSize;

		const stride2 = stride * 2;
		const stride3 = stride * 3;

		const td = t1 - t0;

		const p = (t - t0) / td;
		const pp = p * p;
		const ppp = pp * p;

		const offset1 = i1 * stride3;
		const offset0 = offset1 - stride3;

		const s2 = - 2 * ppp + 3 * pp;
		const s3 = ppp - pp;
		const s0 = 1 - s2;
		const s1 = s3 - pp + p;

		// Layout of keyframe output values for CUBICSPLINE animations:
		//   [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ]
		for (let i = 0; i !== stride; i++) {

			const p0 = values[offset0 + i + stride]; // splineVertex_k
			const m0 = values[offset0 + i + stride2] * td; // outTangent_k * (t_k+1 - t_k)
			const p1 = values[offset1 + i + stride]; // splineVertex_k+1
			const m1 = values[offset1 + i] * td; // inTangent_k+1 * (t_k+1 - t_k)

			result[i] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1;

		}

		return result;

	}

}

const _q = new Quaternion();

class GLTFCubicSplineQuaternionInterpolant extends GLTFCubicSplineInterpolant {

	interpolate_(i1, t0, t, t1) {

		const result = super.interpolate_(i1, t0, t, t1);

		_q.fromArray(result).normalize().toArray(result);

		return result;

	}

}


/*********************************/
/********** INTERNALS ************/
/*********************************/

/* CONSTANTS */

const WEBGL_CONSTANTS = {
	FLOAT: 5126,
	//FLOAT_MAT2: 35674,
	FLOAT_MAT3: 35675,
	FLOAT_MAT4: 35676,
	FLOAT_VEC2: 35664,
	FLOAT_VEC3: 35665,
	FLOAT_VEC4: 35666,
	LINEAR: 9729,
	REPEAT: 10497,
	SAMPLER_2D: 35678,
	POINTS: 0,
	LINES: 1,
	LINE_LOOP: 2,
	LINE_STRIP: 3,
	TRIANGLES: 4,
	TRIANGLE_STRIP: 5,
	TRIANGLE_FAN: 6,
	UNSIGNED_BYTE: 5121,
	UNSIGNED_SHORT: 5123
};

const WEBGL_COMPONENT_TYPES = {
	5120: Int8Array,
	5121: Uint8Array,
	5122: Int16Array,
	5123: Uint16Array,
	5125: Uint32Array,
	5126: Float32Array
};

const WEBGL_FILTERS = {
	9728: NearestFilter,
	9729: LinearFilter,
	9984: NearestMipmapNearestFilter,
	9985: LinearMipmapNearestFilter,
	9986: NearestMipmapLinearFilter,
	9987: LinearMipmapLinearFilter
};

const WEBGL_WRAPPINGS = {
	33071: ClampToEdgeWrapping,
	33648: MirroredRepeatWrapping,
	10497: RepeatWrapping
};

const WEBGL_TYPE_SIZES = {
	'SCALAR': 1,
	'VEC2': 2,
	'VEC3': 3,
	'VEC4': 4,
	'MAT2': 4,
	'MAT3': 9,
	'MAT4': 16
};

const ATTRIBUTES = {
	POSITION: 'position',
	NORMAL: 'normal',
	TANGENT: 'tangent',
	TEXCOORD_0: 'uv',
	TEXCOORD_1: 'uv1',
	TEXCOORD_2: 'uv2',
	TEXCOORD_3: 'uv3',
	COLOR_0: 'color',
	WEIGHTS_0: 'skinWeight',
	JOINTS_0: 'skinIndex',
};

const PATH_PROPERTIES = {
	scale: 'scale',
	translation: 'position',
	rotation: 'quaternion',
	weights: 'morphTargetInfluences'
};

const INTERPOLATION = {
	CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each
	// keyframe track will be initialized with a default interpolation type, then modified.
	LINEAR: InterpolateLinear,
	STEP: InterpolateDiscrete
};

const ALPHA_MODES = {
	OPAQUE: 'OPAQUE',
	MASK: 'MASK',
	BLEND: 'BLEND'
};

/**
 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
 */
function createDefaultMaterial(cache) {

	if (cache['DefaultMaterial'] === undefined) {

		cache['DefaultMaterial'] = new MeshStandardMaterial({
			color: 0xFFFFFF,
			emissive: 0x000000,
			metalness: 1,
			roughness: 1,
			transparent: false,
			depthTest: true,
			side: FrontSide
		});

	}

	return cache['DefaultMaterial'];

}

function addUnknownExtensionsToUserData(knownExtensions, object, objectDef) {

	// Add unknown glTF extensions to an object's userData.

	for (const name in objectDef.extensions) {

		if (knownExtensions[name] === undefined) {

			object.userData.gltfExtensions = object.userData.gltfExtensions || {};
			object.userData.gltfExtensions[name] = objectDef.extensions[name];

		}

	}

}

/**
 * @param {Object3D|Material|BufferGeometry} object
 * @param {GLTF.definition} gltfDef
 */
function assignExtrasToUserData(object, gltfDef) {

	if (gltfDef.extras !== undefined) {

		if (typeof gltfDef.extras === 'object') {

			Object.assign(object.userData, gltfDef.extras);

		} else {

			console.warn('THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras);

		}

	}

}

/**
 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
 *
 * @param {BufferGeometry} geometry
 * @param {Array<GLTF.Target>} targets
 * @param {GLTFParser} parser
 * @return {Promise<BufferGeometry>}
 */
function addMorphTargets(geometry, targets, parser) {

	let hasMorphPosition = false;
	let hasMorphNormal = false;
	let hasMorphColor = false;

	for (let i = 0, il = targets.length; i < il; i++) {

		const target = targets[i];

		if (target.POSITION !== undefined) hasMorphPosition = true;
		if (target.NORMAL !== undefined) hasMorphNormal = true;
		if (target.COLOR_0 !== undefined) hasMorphColor = true;

		if (hasMorphPosition && hasMorphNormal && hasMorphColor) break;

	}

	if (!hasMorphPosition && !hasMorphNormal && !hasMorphColor) return Promise.resolve(geometry);

	const pendingPositionAccessors = [];
	const pendingNormalAccessors = [];
	const pendingColorAccessors = [];

	for (let i = 0, il = targets.length; i < il; i++) {

		const target = targets[i];

		if (hasMorphPosition) {

			const pendingAccessor = target.POSITION !== undefined
				? parser.getDependency('accessor', target.POSITION)
				: geometry.attributes.position;

			pendingPositionAccessors.push(pendingAccessor);

		}

		if (hasMorphNormal) {

			const pendingAccessor = target.NORMAL !== undefined
				? parser.getDependency('accessor', target.NORMAL)
				: geometry.attributes.normal;

			pendingNormalAccessors.push(pendingAccessor);

		}

		if (hasMorphColor) {

			const pendingAccessor = target.COLOR_0 !== undefined
				? parser.getDependency('accessor', target.COLOR_0)
				: geometry.attributes.color;

			pendingColorAccessors.push(pendingAccessor);

		}

	}

	return Promise.all([
		Promise.all(pendingPositionAccessors),
		Promise.all(pendingNormalAccessors),
		Promise.all(pendingColorAccessors)
	]).then(function (accessors) {

		const morphPositions = accessors[0];
		const morphNormals = accessors[1];
		const morphColors = accessors[2];

		if (hasMorphPosition) geometry.morphAttributes.position = morphPositions;
		if (hasMorphNormal) geometry.morphAttributes.normal = morphNormals;
		if (hasMorphColor) geometry.morphAttributes.color = morphColors;
		geometry.morphTargetsRelative = true;

		return geometry;

	});

}

/**
 * @param {Mesh} mesh
 * @param {GLTF.Mesh} meshDef
 */
function updateMorphTargets(mesh, meshDef) {

	mesh.updateMorphTargets();

	if (meshDef.weights !== undefined) {

		for (let i = 0, il = meshDef.weights.length; i < il; i++) {

			mesh.morphTargetInfluences[i] = meshDef.weights[i];

		}

	}

	// .extras has user-defined data, so check that .extras.targetNames is an array.
	if (meshDef.extras && Array.isArray(meshDef.extras.targetNames)) {

		const targetNames = meshDef.extras.targetNames;

		if (mesh.morphTargetInfluences.length === targetNames.length) {

			mesh.morphTargetDictionary = {};

			for (let i = 0, il = targetNames.length; i < il; i++) {

				mesh.morphTargetDictionary[targetNames[i]] = i;

			}

		} else {

			console.warn('THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.');

		}

	}

}

function createPrimitiveKey(primitiveDef) {

	let geometryKey;

	const dracoExtension = primitiveDef.extensions && primitiveDef.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION];

	if (dracoExtension) {

		geometryKey = 'draco:' + dracoExtension.bufferView
			+ ':' + dracoExtension.indices
			+ ':' + createAttributesKey(dracoExtension.attributes);

	} else {

		geometryKey = primitiveDef.indices + ':' + createAttributesKey(primitiveDef.attributes) + ':' + primitiveDef.mode;

	}

	if (primitiveDef.targets !== undefined) {

		for (let i = 0, il = primitiveDef.targets.length; i < il; i++) {

			geometryKey += ':' + createAttributesKey(primitiveDef.targets[i]);

		}

	}

	return geometryKey;

}

function createAttributesKey(attributes) {

	let attributesKey = '';

	const keys = Object.keys(attributes).sort();

	for (let i = 0, il = keys.length; i < il; i++) {

		attributesKey += keys[i] + ':' + attributes[keys[i]] + ';';

	}

	return attributesKey;

}

function getNormalizedComponentScale(constructor) {

	// Reference:
	// https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization#encoding-quantized-data

	switch (constructor) {

		case Int8Array:
			return 1 / 127;

		case Uint8Array:
			return 1 / 255;

		case Int16Array:
			return 1 / 32767;

		case Uint16Array:
			return 1 / 65535;

		default:
			throw new Error('THREE.GLTFLoader: Unsupported normalized accessor component type.');

	}

}

function getImageURIMimeType(uri) {

	if (uri.search(/\.jpe?g($|\?)/i) > 0 || uri.search(/^data\:image\/jpeg/) === 0) return 'image/jpeg';
	if (uri.search(/\.webp($|\?)/i) > 0 || uri.search(/^data\:image\/webp/) === 0) return 'image/webp';
	if (uri.search(/\.ktx2($|\?)/i) > 0 || uri.search(/^data\:image\/ktx2/) === 0) return 'image/ktx2';

	return 'image/png';

}

const _identityMatrix = new Matrix4();

/* GLTF PARSER */

class GLTFParser {

	constructor(json = {}, options = {}) {

		this.json = json;
		this.extensions = {};
		this.plugins = {};
		this.options = options;

		// loader object cache
		this.cache = new GLTFRegistry();

		// associations between Three.js objects and glTF elements
		this.associations = new Map();

		// BufferGeometry caching
		this.primitiveCache = {};

		// Node cache
		this.nodeCache = {};

		// Object3D instance caches
		this.meshCache = { refs: {}, uses: {} };
		this.cameraCache = { refs: {}, uses: {} };
		this.lightCache = { refs: {}, uses: {} };

		this.sourceCache = {};
		this.textureCache = {};

		// Track node names, to ensure no duplicates
		this.nodeNamesUsed = {};

		// Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the
		// expensive work of uploading a texture to the GPU off the main thread.

		let isSafari = false;
		let safariVersion = - 1;
		let isFirefox = false;
		let firefoxVersion = - 1;

		if (typeof navigator !== 'undefined') {

			const userAgent = navigator.userAgent;

			isSafari = /^((?!chrome|android).)*safari/i.test(userAgent) === true;
			const safariMatch = userAgent.match(/Version\/(\d+)/);
			safariVersion = isSafari && safariMatch ? parseInt(safariMatch[1], 10) : - 1;

			isFirefox = userAgent.indexOf('Firefox') > - 1;
			firefoxVersion = isFirefox ? userAgent.match(/Firefox\/([0-9]+)\./)[1] : - 1;

		}

		if (typeof createImageBitmap === 'undefined' || (isSafari && safariVersion < 17) || (isFirefox && firefoxVersion < 98)) {

			this.textureLoader = new TextureLoader(this.options.manager);

		} else {

			this.textureLoader = new ImageBitmapLoader(this.options.manager);

		}

		this.textureLoader.setCrossOrigin(this.options.crossOrigin);
		this.textureLoader.setRequestHeader(this.options.requestHeader);

		this.fileLoader = new FileLoader(this.options.manager);
		this.fileLoader.setResponseType('arraybuffer');

		if (this.options.crossOrigin === 'use-credentials') {

			this.fileLoader.setWithCredentials(true);

		}

	}

	setExtensions(extensions) {

		this.extensions = extensions;

	}

	setPlugins(plugins) {

		this.plugins = plugins;

	}

	parse(onLoad, onError) {

		const parser = this;
		const json = this.json;
		const extensions = this.extensions;

		// Clear the loader cache
		this.cache.removeAll();
		this.nodeCache = {};

		// Mark the special nodes/meshes in json for efficient parse
		this._invokeAll(function (ext) {

			return ext._markDefs && ext._markDefs();

		});

		Promise.all(this._invokeAll(function (ext) {

			return ext.beforeRoot && ext.beforeRoot();

		})).then(function () {

			return Promise.all([

				parser.getDependencies('scene'),
				parser.getDependencies('animation'),
				parser.getDependencies('camera'),

			]);

		}).then(function (dependencies) {

			const result = {
				scene: dependencies[0][json.scene || 0],
				scenes: dependencies[0],
				animations: dependencies[1],
				cameras: dependencies[2],
				asset: json.asset,
				parser: parser,
				userData: {}
			};

			addUnknownExtensionsToUserData(extensions, result, json);

			assignExtrasToUserData(result, json);

			return Promise.all(parser._invokeAll(function (ext) {

				return ext.afterRoot && ext.afterRoot(result);

			})).then(function () {

				for (const scene of result.scenes) {

					scene.updateMatrixWorld();

				}

				onLoad(result);

			});

		}).catch(onError);

	}

	/**
	 * Marks the special nodes/meshes in json for efficient parse.
	 */
	_markDefs() {

		const nodeDefs = this.json.nodes || [];
		const skinDefs = this.json.skins || [];
		const meshDefs = this.json.meshes || [];

		// Nothing in the node definition indicates whether it is a Bone or an
		// Object3D. Use the skins' joint references to mark bones.
		for (let skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex++) {

			const joints = skinDefs[skinIndex].joints;

			for (let i = 0, il = joints.length; i < il; i++) {

				nodeDefs[joints[i]].isBone = true;

			}

		}

		// Iterate over all nodes, marking references to shared resources,
		// as well as skeleton joints.
		for (let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) {

			const nodeDef = nodeDefs[nodeIndex];

			if (nodeDef.mesh !== undefined) {

				this._addNodeRef(this.meshCache, nodeDef.mesh);

				// Nothing in the mesh definition indicates whether it is
				// a SkinnedMesh or Mesh. Use the node's mesh reference
				// to mark SkinnedMesh if node has skin.
				if (nodeDef.skin !== undefined) {

					meshDefs[nodeDef.mesh].isSkinnedMesh = true;

				}

			}

			if (nodeDef.camera !== undefined) {

				this._addNodeRef(this.cameraCache, nodeDef.camera);

			}

		}

	}

	/**
	 * Counts references to shared node / Object3D resources. These resources
	 * can be reused, or "instantiated", at multiple nodes in the scene
	 * hierarchy. Mesh, Camera, and Light instances are instantiated and must
	 * be marked. Non-scenegraph resources (like Materials, Geometries, and
	 * Textures) can be reused directly and are not marked here.
	 *
	 * Example: CesiumMilkTruck sample model reuses "Wheel" meshes.
	 */
	_addNodeRef(cache, index) {

		if (index === undefined) return;

		if (cache.refs[index] === undefined) {

			cache.refs[index] = cache.uses[index] = 0;

		}

		cache.refs[index]++;

	}

	/** Returns a reference to a shared resource, cloning it if necessary. */
	_getNodeRef(cache, index, object) {

		if (cache.refs[index] <= 1) return object;

		const ref = object.clone();

		// Propagates mappings to the cloned object, prevents mappings on the
		// original object from being lost.
		const updateMappings = (original, clone) => {

			const mappings = this.associations.get(original);
			if (mappings != null) {

				this.associations.set(clone, mappings);

			}

			for (const [i, child] of original.children.entries()) {

				updateMappings(child, clone.children[i]);

			}

		};

		updateMappings(object, ref);

		ref.name += '_instance_' + (cache.uses[index]++);

		return ref;

	}

	_invokeOne(func) {

		const extensions = Object.values(this.plugins);
		extensions.push(this);

		for (let i = 0; i < extensions.length; i++) {

			const result = func(extensions[i]);

			if (result) return result;

		}

		return null;

	}

	_invokeAll(func) {

		const extensions = Object.values(this.plugins);
		extensions.unshift(this);

		const pending = [];

		for (let i = 0; i < extensions.length; i++) {

			const result = func(extensions[i]);

			if (result) pending.push(result);

		}

		return pending;

	}

	/**
	 * Requests the specified dependency asynchronously, with caching.
	 * @param {string} type
	 * @param {number} index
	 * @return {Promise<Object3D|Material|THREE.Texture|AnimationClip|ArrayBuffer|Object>}
	 */
	getDependency(type, index) {

		const cacheKey = type + ':' + index;
		let dependency = this.cache.get(cacheKey);

		if (!dependency) {

			switch (type) {

				case 'scene':
					dependency = this.loadScene(index);
					break;

				case 'node':
					dependency = this._invokeOne(function (ext) {

						return ext.loadNode && ext.loadNode(index);

					});
					break;

				case 'mesh':
					dependency = this._invokeOne(function (ext) {

						return ext.loadMesh && ext.loadMesh(index);

					});
					break;

				case 'accessor':
					dependency = this.loadAccessor(index);
					break;

				case 'bufferView':
					dependency = this._invokeOne(function (ext) {

						return ext.loadBufferView && ext.loadBufferView(index);

					});
					break;

				case 'buffer':
					dependency = this.loadBuffer(index);
					break;

				case 'material':
					dependency = this._invokeOne(function (ext) {

						return ext.loadMaterial && ext.loadMaterial(index);

					});
					break;

				case 'texture':
					dependency = this._invokeOne(function (ext) {

						return ext.loadTexture && ext.loadTexture(index);

					});
					break;

				case 'skin':
					dependency = this.loadSkin(index);
					break;

				case 'animation':
					dependency = this._invokeOne(function (ext) {

						return ext.loadAnimation && ext.loadAnimation(index);

					});
					break;

				case 'camera':
					dependency = this.loadCamera(index);
					break;

				default:
					dependency = this._invokeOne(function (ext) {

						return ext != this && ext.getDependency && ext.getDependency(type, index);

					});

					if (!dependency) {

						throw new Error('Unknown type: ' + type);

					}

					break;

			}

			this.cache.add(cacheKey, dependency);

		}

		return dependency;

	}

	/**
	 * Requests all dependencies of the specified type asynchronously, with caching.
	 * @param {string} type
	 * @return {Promise<Array<Object>>}
	 */
	getDependencies(type) {

		let dependencies = this.cache.get(type);

		if (!dependencies) {

			const parser = this;
			const defs = this.json[type + (type === 'mesh' ? 'es' : 's')] || [];

			dependencies = Promise.all(defs.map(function (def, index) {

				return parser.getDependency(type, index);

			}));

			this.cache.add(type, dependencies);

		}

		return dependencies;

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
	 * @param {number} bufferIndex
	 * @return {Promise<ArrayBuffer>}
	 */
	loadBuffer(bufferIndex) {

		const bufferDef = this.json.buffers[bufferIndex];
		const loader = this.fileLoader;

		if (bufferDef.type && bufferDef.type !== 'arraybuffer') {

			throw new Error('THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.');

		}

		// If present, GLB container is required to be the first buffer.
		if (bufferDef.uri === undefined && bufferIndex === 0) {

			return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body);

		}

		const options = this.options;

		return new Promise(function (resolve, reject) {

			loader.load(LoaderUtils.resolveURL(bufferDef.uri, options.path), resolve, undefined, function () {

				reject(new Error('THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".'));

			});

		});

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
	 * @param {number} bufferViewIndex
	 * @return {Promise<ArrayBuffer>}
	 */
	loadBufferView(bufferViewIndex) {

		const bufferViewDef = this.json.bufferViews[bufferViewIndex];

		return this.getDependency('buffer', bufferViewDef.buffer).then(function (buffer) {

			const byteLength = bufferViewDef.byteLength || 0;
			const byteOffset = bufferViewDef.byteOffset || 0;
			return buffer.slice(byteOffset, byteOffset + byteLength);

		});

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors
	 * @param {number} accessorIndex
	 * @return {Promise<BufferAttribute|InterleavedBufferAttribute>}
	 */
	loadAccessor(accessorIndex) {

		const parser = this;
		const json = this.json;

		const accessorDef = this.json.accessors[accessorIndex];

		if (accessorDef.bufferView === undefined && accessorDef.sparse === undefined) {

			const itemSize = WEBGL_TYPE_SIZES[accessorDef.type];
			const TypedArray = WEBGL_COMPONENT_TYPES[accessorDef.componentType];
			const normalized = accessorDef.normalized === true;

			const array = new TypedArray(accessorDef.count * itemSize);
			return Promise.resolve(new BufferAttribute(array, itemSize, normalized));

		}

		const pendingBufferViews = [];

		if (accessorDef.bufferView !== undefined) {

			pendingBufferViews.push(this.getDependency('bufferView', accessorDef.bufferView));

		} else {

			pendingBufferViews.push(null);

		}

		if (accessorDef.sparse !== undefined) {

			pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.indices.bufferView));
			pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.values.bufferView));

		}

		return Promise.all(pendingBufferViews).then(function (bufferViews) {

			const bufferView = bufferViews[0];

			const itemSize = WEBGL_TYPE_SIZES[accessorDef.type];
			const TypedArray = WEBGL_COMPONENT_TYPES[accessorDef.componentType];

			// For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12.
			const elementBytes = TypedArray.BYTES_PER_ELEMENT;
			const itemBytes = elementBytes * itemSize;
			const byteOffset = accessorDef.byteOffset || 0;
			const byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[accessorDef.bufferView].byteStride : undefined;
			const normalized = accessorDef.normalized === true;
			let array, bufferAttribute;

			// The buffer is not interleaved if the stride is the item size in bytes.
			if (byteStride && byteStride !== itemBytes) {

				// Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer
				// This makes sure that IBA.count reflects accessor.count properly
				const ibSlice = Math.floor(byteOffset / byteStride);
				const ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count;
				let ib = parser.cache.get(ibCacheKey);

				if (!ib) {

					array = new TypedArray(bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes);

					// Integer parameters to IB/IBA are in array elements, not bytes.
					ib = new InterleavedBuffer(array, byteStride / elementBytes);

					parser.cache.add(ibCacheKey, ib);

				}

				bufferAttribute = new InterleavedBufferAttribute(ib, itemSize, (byteOffset % byteStride) / elementBytes, normalized);

			} else {

				if (bufferView === null) {

					array = new TypedArray(accessorDef.count * itemSize);

				} else {

					array = new TypedArray(bufferView, byteOffset, accessorDef.count * itemSize);

				}

				bufferAttribute = new BufferAttribute(array, itemSize, normalized);

			}

			// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors
			if (accessorDef.sparse !== undefined) {

				const itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR;
				const TypedArrayIndices = WEBGL_COMPONENT_TYPES[accessorDef.sparse.indices.componentType];

				const byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0;
				const byteOffsetValues = accessorDef.sparse.values.byteOffset || 0;

				const sparseIndices = new TypedArrayIndices(bufferViews[1], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices);
				const sparseValues = new TypedArray(bufferViews[2], byteOffsetValues, accessorDef.sparse.count * itemSize);

				if (bufferView !== null) {

					// Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes.
					bufferAttribute = new BufferAttribute(bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized);

				}

				// Ignore normalized since we copy from sparse
				bufferAttribute.normalized = false;

				for (let i = 0, il = sparseIndices.length; i < il; i++) {

					const index = sparseIndices[i];

					bufferAttribute.setX(index, sparseValues[i * itemSize]);
					if (itemSize >= 2) bufferAttribute.setY(index, sparseValues[i * itemSize + 1]);
					if (itemSize >= 3) bufferAttribute.setZ(index, sparseValues[i * itemSize + 2]);
					if (itemSize >= 4) bufferAttribute.setW(index, sparseValues[i * itemSize + 3]);
					if (itemSize >= 5) throw new Error('THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.');

				}

				bufferAttribute.normalized = normalized;

			}

			return bufferAttribute;

		});

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures
	 * @param {number} textureIndex
	 * @return {Promise<THREE.Texture|null>}
	 */
	loadTexture(textureIndex) {

		const json = this.json;
		const options = this.options;
		const textureDef = json.textures[textureIndex];
		const sourceIndex = textureDef.source;
		const sourceDef = json.images[sourceIndex];

		let loader = this.textureLoader;

		if (sourceDef.uri) {

			const handler = options.manager.getHandler(sourceDef.uri);
			if (handler !== null) loader = handler;

		}

		return this.loadTextureImage(textureIndex, sourceIndex, loader);

	}

	loadTextureImage(textureIndex, sourceIndex, loader) {

		const parser = this;
		const json = this.json;

		const textureDef = json.textures[textureIndex];
		const sourceDef = json.images[sourceIndex];

		const cacheKey = (sourceDef.uri || sourceDef.bufferView) + ':' + textureDef.sampler;

		if (this.textureCache[cacheKey]) {

			// See https://github.com/mrdoob/three.js/issues/21559.
			return this.textureCache[cacheKey];

		}

		const promise = this.loadImageSource(sourceIndex, loader).then(function (texture) {

			texture.flipY = false;

			texture.name = textureDef.name || sourceDef.name || '';

			if (texture.name === '' && typeof sourceDef.uri === 'string' && sourceDef.uri.startsWith('data:image/') === false) {

				texture.name = sourceDef.uri;

			}

			const samplers = json.samplers || {};
			const sampler = samplers[textureDef.sampler] || {};

			texture.magFilter = WEBGL_FILTERS[sampler.magFilter] || LinearFilter;
			texture.minFilter = WEBGL_FILTERS[sampler.minFilter] || LinearMipmapLinearFilter;
			texture.wrapS = WEBGL_WRAPPINGS[sampler.wrapS] || RepeatWrapping;
			texture.wrapT = WEBGL_WRAPPINGS[sampler.wrapT] || RepeatWrapping;
			texture.generateMipmaps = !texture.isCompressedTexture && texture.minFilter !== NearestFilter && texture.minFilter !== LinearFilter;

			parser.associations.set(texture, { textures: textureIndex });

			return texture;

		}).catch(function () {

			return null;

		});

		this.textureCache[cacheKey] = promise;

		return promise;

	}

	loadImageSource(sourceIndex, loader) {

		const parser = this;
		const json = this.json;
		const options = this.options;

		if (this.sourceCache[sourceIndex] !== undefined) {

			return this.sourceCache[sourceIndex].then((texture) => texture.clone());

		}

		const sourceDef = json.images[sourceIndex];

		const URL = self.URL || self.webkitURL;

		let sourceURI = sourceDef.uri || '';
		let isObjectURL = false;

		if (sourceDef.bufferView !== undefined) {

			// Load binary image data from bufferView, if provided.

			sourceURI = parser.getDependency('bufferView', sourceDef.bufferView).then(function (bufferView) {

				isObjectURL = true;
				const blob = new Blob([bufferView], { type: sourceDef.mimeType });
				sourceURI = URL.createObjectURL(blob);
				return sourceURI;

			});

		} else if (sourceDef.uri === undefined) {

			throw new Error('THREE.GLTFLoader: Image ' + sourceIndex + ' is missing URI and bufferView');

		}

		const promise = Promise.resolve(sourceURI).then(function (sourceURI) {

			return new Promise(function (resolve, reject) {

				let onLoad = resolve;

				if (loader.isImageBitmapLoader === true) {

					onLoad = function (imageBitmap) {

						const texture = new Texture(imageBitmap);
						texture.needsUpdate = true;

						resolve(texture);

					};

				}

				loader.load(LoaderUtils.resolveURL(sourceURI, options.path), onLoad, undefined, reject);

			});

		}).then(function (texture) {

			// Clean up resources and configure Texture.

			if (isObjectURL === true) {

				URL.revokeObjectURL(sourceURI);

			}

			assignExtrasToUserData(texture, sourceDef);

			texture.userData.mimeType = sourceDef.mimeType || getImageURIMimeType(sourceDef.uri);

			return texture;

		}).catch(function (error) {

			console.error('THREE.GLTFLoader: Couldn\'t load texture', sourceURI);
			throw error;

		});

		this.sourceCache[sourceIndex] = promise;
		return promise;

	}

	/**
	 * Asynchronously assigns a texture to the given material parameters.
	 * @param {Object} materialParams
	 * @param {string} mapName
	 * @param {Object} mapDef
	 * @return {Promise<Texture>}
	 */
	assignTexture(materialParams, mapName, mapDef, colorSpace) {

		const parser = this;

		return this.getDependency('texture', mapDef.index).then(function (texture) {

			if (!texture) return null;

			if (mapDef.texCoord !== undefined && mapDef.texCoord > 0) {

				texture = texture.clone();
				texture.channel = mapDef.texCoord;

			}

			if (parser.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM]) {

				const transform = mapDef.extensions !== undefined ? mapDef.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM] : undefined;

				if (transform) {

					const gltfReference = parser.associations.get(texture);
					texture = parser.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM].extendTexture(texture, transform);
					parser.associations.set(texture, gltfReference);

				}

			}

			if (colorSpace !== undefined) {

				texture.colorSpace = colorSpace;

			}

			materialParams[mapName] = texture;

			return texture;

		});

	}

	/**
	 * Assigns final material to a Mesh, Line, or Points instance. The instance
	 * already has a material (generated from the glTF material options alone)
	 * but reuse of the same glTF material may require multiple threejs materials
	 * to accommodate different primitive types, defines, etc. New materials will
	 * be created if necessary, and reused from a cache.
	 * @param  {Object3D} mesh Mesh, Line, or Points instance.
	 */
	assignFinalMaterial(mesh) {

		const geometry = mesh.geometry;
		let material = mesh.material;

		const useDerivativeTangents = geometry.attributes.tangent === undefined;
		const useVertexColors = geometry.attributes.color !== undefined;
		const useFlatShading = geometry.attributes.normal === undefined;

		if (mesh.isPoints) {

			const cacheKey = 'PointsMaterial:' + material.uuid;

			let pointsMaterial = this.cache.get(cacheKey);

			if (!pointsMaterial) {

				pointsMaterial = new PointsMaterial();
				Material.prototype.copy.call(pointsMaterial, material);
				pointsMaterial.color.copy(material.color);
				pointsMaterial.map = material.map;
				pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px

				this.cache.add(cacheKey, pointsMaterial);

			}

			material = pointsMaterial;

		} else if (mesh.isLine) {

			const cacheKey = 'LineBasicMaterial:' + material.uuid;

			let lineMaterial = this.cache.get(cacheKey);

			if (!lineMaterial) {

				lineMaterial = new LineBasicMaterial();
				Material.prototype.copy.call(lineMaterial, material);
				lineMaterial.color.copy(material.color);
				lineMaterial.map = material.map;

				this.cache.add(cacheKey, lineMaterial);

			}

			material = lineMaterial;

		}

		// Clone the material if it will be modified
		if (useDerivativeTangents || useVertexColors || useFlatShading) {

			let cacheKey = 'ClonedMaterial:' + material.uuid + ':';

			if (useDerivativeTangents) cacheKey += 'derivative-tangents:';
			if (useVertexColors) cacheKey += 'vertex-colors:';
			if (useFlatShading) cacheKey += 'flat-shading:';

			let cachedMaterial = this.cache.get(cacheKey);

			if (!cachedMaterial) {

				cachedMaterial = material.clone();

				if (useVertexColors) cachedMaterial.vertexColors = true;
				if (useFlatShading) cachedMaterial.flatShading = true;

				if (useDerivativeTangents) {

					// https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995
					if (cachedMaterial.normalScale) cachedMaterial.normalScale.y *= - 1;
					if (cachedMaterial.clearcoatNormalScale) cachedMaterial.clearcoatNormalScale.y *= - 1;

				}

				this.cache.add(cacheKey, cachedMaterial);

				this.associations.set(cachedMaterial, this.associations.get(material));

			}

			material = cachedMaterial;

		}

		mesh.material = material;

	}

	getMaterialType( /* materialIndex */) {

		return MeshStandardMaterial;

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials
	 * @param {number} materialIndex
	 * @return {Promise<Material>}
	 */
	loadMaterial(materialIndex) {

		const parser = this;
		const json = this.json;
		const extensions = this.extensions;
		const materialDef = json.materials[materialIndex];

		let materialType;
		const materialParams = {};
		const materialExtensions = materialDef.extensions || {};

		const pending = [];

		if (materialExtensions[EXTENSIONS.KHR_MATERIALS_UNLIT]) {

			const kmuExtension = extensions[EXTENSIONS.KHR_MATERIALS_UNLIT];
			materialType = kmuExtension.getMaterialType();
			pending.push(kmuExtension.extendParams(materialParams, materialDef, parser));

		} else {

			// Specification:
			// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material

			const metallicRoughness = materialDef.pbrMetallicRoughness || {};

			materialParams.color = new Color(1.0, 1.0, 1.0);
			materialParams.opacity = 1.0;

			if (Array.isArray(metallicRoughness.baseColorFactor)) {

				const array = metallicRoughness.baseColorFactor;

				materialParams.color.setRGB(array[0], array[1], array[2], LinearSRGBColorSpace);
				materialParams.opacity = array[3];

			}

			if (metallicRoughness.baseColorTexture !== undefined) {

				pending.push(parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture, SRGBColorSpace));

			}

			materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0;
			materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0;

			if (metallicRoughness.metallicRoughnessTexture !== undefined) {

				pending.push(parser.assignTexture(materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture));
				pending.push(parser.assignTexture(materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture));

			}

			materialType = this._invokeOne(function (ext) {

				return ext.getMaterialType && ext.getMaterialType(materialIndex);

			});

			pending.push(Promise.all(this._invokeAll(function (ext) {

				return ext.extendMaterialParams && ext.extendMaterialParams(materialIndex, materialParams);

			})));

		}

		if (materialDef.doubleSided === true) {

			materialParams.side = DoubleSide;

		}

		const alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE;

		if (alphaMode === ALPHA_MODES.BLEND) {

			materialParams.transparent = true;

			// See: https://github.com/mrdoob/three.js/issues/17706
			materialParams.depthWrite = false;

		} else {

			materialParams.transparent = false;

			if (alphaMode === ALPHA_MODES.MASK) {

				materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5;

			}

		}

		if (materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial) {

			pending.push(parser.assignTexture(materialParams, 'normalMap', materialDef.normalTexture));

			materialParams.normalScale = new Vector2(1, 1);

			if (materialDef.normalTexture.scale !== undefined) {

				const scale = materialDef.normalTexture.scale;

				materialParams.normalScale.set(scale, scale);

			}

		}

		if (materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial) {

			pending.push(parser.assignTexture(materialParams, 'aoMap', materialDef.occlusionTexture));

			if (materialDef.occlusionTexture.strength !== undefined) {

				materialParams.aoMapIntensity = materialDef.occlusionTexture.strength;

			}

		}

		if (materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial) {

			const emissiveFactor = materialDef.emissiveFactor;
			materialParams.emissive = new Color().setRGB(emissiveFactor[0], emissiveFactor[1], emissiveFactor[2], LinearSRGBColorSpace);

		}

		if (materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial) {

			pending.push(parser.assignTexture(materialParams, 'emissiveMap', materialDef.emissiveTexture, SRGBColorSpace));

		}

		return Promise.all(pending).then(function () {

			const material = new materialType(materialParams);

			if (materialDef.name) material.name = materialDef.name;

			assignExtrasToUserData(material, materialDef);

			parser.associations.set(material, { materials: materialIndex });

			if (materialDef.extensions) addUnknownExtensionsToUserData(extensions, material, materialDef);

			return material;

		});

	}

	/** When Object3D instances are targeted by animation, they need unique names. */
	createUniqueName(originalName) {

		const sanitizedName = PropertyBinding.sanitizeNodeName(originalName || '');

		if (sanitizedName in this.nodeNamesUsed) {

			return sanitizedName + '_' + (++this.nodeNamesUsed[sanitizedName]);

		} else {

			this.nodeNamesUsed[sanitizedName] = 0;

			return sanitizedName;

		}

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
	 *
	 * Creates BufferGeometries from primitives.
	 *
	 * @param {Array<GLTF.Primitive>} primitives
	 * @return {Promise<Array<BufferGeometry>>}
	 */
	loadGeometries(primitives) {

		const parser = this;
		const extensions = this.extensions;
		const cache = this.primitiveCache;

		function createDracoPrimitive(primitive) {

			return extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]
				.decodePrimitive(primitive, parser)
				.then(function (geometry) {

					return addPrimitiveAttributes(geometry, primitive, parser);

				});

		}

		const pending = [];

		for (let i = 0, il = primitives.length; i < il; i++) {

			const primitive = primitives[i];
			const cacheKey = createPrimitiveKey(primitive);

			// See if we've already created this geometry
			const cached = cache[cacheKey];

			if (cached) {

				// Use the cached geometry if it exists
				pending.push(cached.promise);

			} else {

				let geometryPromise;

				if (primitive.extensions && primitive.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]) {

					// Use DRACO geometry if available
					geometryPromise = createDracoPrimitive(primitive);

				} else {

					// Otherwise create a new geometry
					geometryPromise = addPrimitiveAttributes(new BufferGeometry(), primitive, parser);

				}

				// Cache this geometry
				cache[cacheKey] = { primitive: primitive, promise: geometryPromise };

				pending.push(geometryPromise);

			}

		}

		return Promise.all(pending);

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes
	 * @param {number} meshIndex
	 * @return {Promise<Group|Mesh|SkinnedMesh>}
	 */
	loadMesh(meshIndex) {

		const parser = this;
		const json = this.json;
		const extensions = this.extensions;

		const meshDef = json.meshes[meshIndex];
		const primitives = meshDef.primitives;

		const pending = [];

		for (let i = 0, il = primitives.length; i < il; i++) {

			const material = primitives[i].material === undefined
				? createDefaultMaterial(this.cache)
				: this.getDependency('material', primitives[i].material);

			pending.push(material);

		}

		pending.push(parser.loadGeometries(primitives));

		return Promise.all(pending).then(function (results) {

			const materials = results.slice(0, results.length - 1);
			const geometries = results[results.length - 1];

			const meshes = [];

			for (let i = 0, il = geometries.length; i < il; i++) {

				const geometry = geometries[i];
				const primitive = primitives[i];

				// 1. create Mesh

				let mesh;

				const material = materials[i];

				if (primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
					primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
					primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
					primitive.mode === undefined) {

					// .isSkinnedMesh isn't in glTF spec. See ._markDefs()
					mesh = meshDef.isSkinnedMesh === true
						? new SkinnedMesh(geometry, material)
						: new Mesh(geometry, material);

					if (mesh.isSkinnedMesh === true) {

						// normalize skin weights to fix malformed assets (see #15319)
						mesh.normalizeSkinWeights();

					}

					if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP) {

						mesh.geometry = toTrianglesDrawMode(mesh.geometry, TriangleStripDrawMode);

					} else if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN) {

						mesh.geometry = toTrianglesDrawMode(mesh.geometry, TriangleFanDrawMode);

					}

				} else if (primitive.mode === WEBGL_CONSTANTS.LINES) {

					mesh = new LineSegments(geometry, material);

				} else if (primitive.mode === WEBGL_CONSTANTS.LINE_STRIP) {

					mesh = new Line(geometry, material);

				} else if (primitive.mode === WEBGL_CONSTANTS.LINE_LOOP) {

					mesh = new LineLoop(geometry, material);

				} else if (primitive.mode === WEBGL_CONSTANTS.POINTS) {

					mesh = new Points(geometry, material);

				} else {

					throw new Error('THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode);

				}

				if (Object.keys(mesh.geometry.morphAttributes).length > 0) {

					updateMorphTargets(mesh, meshDef);

				}

				mesh.name = parser.createUniqueName(meshDef.name || ('mesh_' + meshIndex));

				assignExtrasToUserData(mesh, meshDef);

				if (primitive.extensions) addUnknownExtensionsToUserData(extensions, mesh, primitive);

				parser.assignFinalMaterial(mesh);

				meshes.push(mesh);

			}

			for (let i = 0, il = meshes.length; i < il; i++) {

				parser.associations.set(meshes[i], {
					meshes: meshIndex,
					primitives: i
				});

			}

			if (meshes.length === 1) {

				if (meshDef.extensions) addUnknownExtensionsToUserData(extensions, meshes[0], meshDef);

				return meshes[0];

			}

			const group = new Group();

			if (meshDef.extensions) addUnknownExtensionsToUserData(extensions, group, meshDef);

			parser.associations.set(group, { meshes: meshIndex });

			for (let i = 0, il = meshes.length; i < il; i++) {

				group.add(meshes[i]);

			}

			return group;

		});

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras
	 * @param {number} cameraIndex
	 * @return {Promise<THREE.Camera>}
	 */
	loadCamera(cameraIndex) {

		let camera;
		const cameraDef = this.json.cameras[cameraIndex];
		const params = cameraDef[cameraDef.type];

		if (!params) {

			console.warn('THREE.GLTFLoader: Missing camera parameters.');
			return;

		}

		if (cameraDef.type === 'perspective') {

			camera = new PerspectiveCamera(MathUtils.radToDeg(params.yfov), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6);

		} else if (cameraDef.type === 'orthographic') {

			camera = new OrthographicCamera(- params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar);

		}

		if (cameraDef.name) camera.name = this.createUniqueName(cameraDef.name);

		assignExtrasToUserData(camera, cameraDef);

		return Promise.resolve(camera);

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins
	 * @param {number} skinIndex
	 * @return {Promise<Skeleton>}
	 */
	loadSkin(skinIndex) {

		const skinDef = this.json.skins[skinIndex];

		const pending = [];

		for (let i = 0, il = skinDef.joints.length; i < il; i++) {

			pending.push(this._loadNodeShallow(skinDef.joints[i]));

		}

		if (skinDef.inverseBindMatrices !== undefined) {

			pending.push(this.getDependency('accessor', skinDef.inverseBindMatrices));

		} else {

			pending.push(null);

		}

		return Promise.all(pending).then(function (results) {

			const inverseBindMatrices = results.pop();
			const jointNodes = results;

			// Note that bones (joint nodes) may or may not be in the
			// scene graph at this time.

			const bones = [];
			const boneInverses = [];

			for (let i = 0, il = jointNodes.length; i < il; i++) {

				const jointNode = jointNodes[i];

				if (jointNode) {

					bones.push(jointNode);

					const mat = new Matrix4();

					if (inverseBindMatrices !== null) {

						mat.fromArray(inverseBindMatrices.array, i * 16);

					}

					boneInverses.push(mat);

				} else {

					console.warn('THREE.GLTFLoader: Joint "%s" could not be found.', skinDef.joints[i]);

				}

			}

			return new Skeleton(bones, boneInverses);

		});

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
	 * @param {number} animationIndex
	 * @return {Promise<AnimationClip>}
	 */
	loadAnimation(animationIndex) {

		const json = this.json;
		const parser = this;

		const animationDef = json.animations[animationIndex];
		const animationName = animationDef.name ? animationDef.name : 'animation_' + animationIndex;

		const pendingNodes = [];
		const pendingInputAccessors = [];
		const pendingOutputAccessors = [];
		const pendingSamplers = [];
		const pendingTargets = [];

		for (let i = 0, il = animationDef.channels.length; i < il; i++) {

			const channel = animationDef.channels[i];
			const sampler = animationDef.samplers[channel.sampler];
			const target = channel.target;
			const name = target.node;
			const input = animationDef.parameters !== undefined ? animationDef.parameters[sampler.input] : sampler.input;
			const output = animationDef.parameters !== undefined ? animationDef.parameters[sampler.output] : sampler.output;

			if (target.node === undefined) continue;

			pendingNodes.push(this.getDependency('node', name));
			pendingInputAccessors.push(this.getDependency('accessor', input));
			pendingOutputAccessors.push(this.getDependency('accessor', output));
			pendingSamplers.push(sampler);
			pendingTargets.push(target);

		}

		return Promise.all([

			Promise.all(pendingNodes),
			Promise.all(pendingInputAccessors),
			Promise.all(pendingOutputAccessors),
			Promise.all(pendingSamplers),
			Promise.all(pendingTargets)

		]).then(function (dependencies) {

			const nodes = dependencies[0];
			const inputAccessors = dependencies[1];
			const outputAccessors = dependencies[2];
			const samplers = dependencies[3];
			const targets = dependencies[4];

			const tracks = [];

			for (let i = 0, il = nodes.length; i < il; i++) {

				const node = nodes[i];
				const inputAccessor = inputAccessors[i];
				const outputAccessor = outputAccessors[i];
				const sampler = samplers[i];
				const target = targets[i];

				if (node === undefined) continue;

				if (node.updateMatrix) {

					node.updateMatrix();

				}

				const createdTracks = parser._createAnimationTracks(node, inputAccessor, outputAccessor, sampler, target);

				if (createdTracks) {

					for (let k = 0; k < createdTracks.length; k++) {

						tracks.push(createdTracks[k]);

					}

				}

			}

			return new AnimationClip(animationName, undefined, tracks);

		});

	}

	createNodeMesh(nodeIndex) {

		const json = this.json;
		const parser = this;
		const nodeDef = json.nodes[nodeIndex];

		if (nodeDef.mesh === undefined) return null;

		return parser.getDependency('mesh', nodeDef.mesh).then(function (mesh) {

			const node = parser._getNodeRef(parser.meshCache, nodeDef.mesh, mesh);

			// if weights are provided on the node, override weights on the mesh.
			if (nodeDef.weights !== undefined) {

				node.traverse(function (o) {

					if (!o.isMesh) return;

					for (let i = 0, il = nodeDef.weights.length; i < il; i++) {

						o.morphTargetInfluences[i] = nodeDef.weights[i];

					}

				});

			}

			return node;

		});

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
	 * @param {number} nodeIndex
	 * @return {Promise<Object3D>}
	 */
	loadNode(nodeIndex) {

		const json = this.json;
		const parser = this;

		const nodeDef = json.nodes[nodeIndex];

		const nodePending = parser._loadNodeShallow(nodeIndex);

		const childPending = [];
		const childrenDef = nodeDef.children || [];

		for (let i = 0, il = childrenDef.length; i < il; i++) {

			childPending.push(parser.getDependency('node', childrenDef[i]));

		}

		const skeletonPending = nodeDef.skin === undefined
			? Promise.resolve(null)
			: parser.getDependency('skin', nodeDef.skin);

		return Promise.all([
			nodePending,
			Promise.all(childPending),
			skeletonPending
		]).then(function (results) {

			const node = results[0];
			const children = results[1];
			const skeleton = results[2];

			if (skeleton !== null) {

				// This full traverse should be fine because
				// child glTF nodes have not been added to this node yet.
				node.traverse(function (mesh) {

					if (!mesh.isSkinnedMesh) return;

					mesh.bind(skeleton, _identityMatrix);

				});

			}

			for (let i = 0, il = children.length; i < il; i++) {

				node.add(children[i]);

			}

			return node;

		});

	}

	// ._loadNodeShallow() parses a single node.
	// skin and child nodes are created and added in .loadNode() (no '_' prefix).
	_loadNodeShallow(nodeIndex) {

		const json = this.json;
		const extensions = this.extensions;
		const parser = this;

		// This method is called from .loadNode() and .loadSkin().
		// Cache a node to avoid duplication.

		if (this.nodeCache[nodeIndex] !== undefined) {

			return this.nodeCache[nodeIndex];

		}

		const nodeDef = json.nodes[nodeIndex];

		// reserve node's name before its dependencies, so the root has the intended name.
		const nodeName = nodeDef.name ? parser.createUniqueName(nodeDef.name) : '';

		const pending = [];

		const meshPromise = parser._invokeOne(function (ext) {

			return ext.createNodeMesh && ext.createNodeMesh(nodeIndex);

		});

		if (meshPromise) {

			pending.push(meshPromise);

		}

		if (nodeDef.camera !== undefined) {

			pending.push(parser.getDependency('camera', nodeDef.camera).then(function (camera) {

				return parser._getNodeRef(parser.cameraCache, nodeDef.camera, camera);

			}));

		}

		parser._invokeAll(function (ext) {

			return ext.createNodeAttachment && ext.createNodeAttachment(nodeIndex);

		}).forEach(function (promise) {

			pending.push(promise);

		});

		this.nodeCache[nodeIndex] = Promise.all(pending).then(function (objects) {

			let node;

			// .isBone isn't in glTF spec. See ._markDefs
			if (nodeDef.isBone === true) {

				node = new Bone();

			} else if (objects.length > 1) {

				node = new Group();

			} else if (objects.length === 1) {

				node = objects[0];

			} else {

				node = new Object3D();

			}

			if (node !== objects[0]) {

				for (let i = 0, il = objects.length; i < il; i++) {

					node.add(objects[i]);

				}

			}

			if (nodeDef.name) {

				node.userData.name = nodeDef.name;
				node.name = nodeName;

			}

			assignExtrasToUserData(node, nodeDef);

			if (nodeDef.extensions) addUnknownExtensionsToUserData(extensions, node, nodeDef);

			if (nodeDef.matrix !== undefined) {

				const matrix = new Matrix4();
				matrix.fromArray(nodeDef.matrix);
				node.applyMatrix4(matrix);

			} else {

				if (nodeDef.translation !== undefined) {

					node.position.fromArray(nodeDef.translation);

				}

				if (nodeDef.rotation !== undefined) {

					node.quaternion.fromArray(nodeDef.rotation);

				}

				if (nodeDef.scale !== undefined) {

					node.scale.fromArray(nodeDef.scale);

				}

			}

			if (!parser.associations.has(node)) {

				parser.associations.set(node, {});

			}

			parser.associations.get(node).nodes = nodeIndex;

			return node;

		});

		return this.nodeCache[nodeIndex];

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes
	 * @param {number} sceneIndex
	 * @return {Promise<Group>}
	 */
	loadScene(sceneIndex) {

		const extensions = this.extensions;
		const sceneDef = this.json.scenes[sceneIndex];
		const parser = this;

		// Loader returns Group, not Scene.
		// See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172
		const scene = new Group();
		if (sceneDef.name) scene.name = parser.createUniqueName(sceneDef.name);

		assignExtrasToUserData(scene, sceneDef);

		if (sceneDef.extensions) addUnknownExtensionsToUserData(extensions, scene, sceneDef);

		const nodeIds = sceneDef.nodes || [];

		const pending = [];

		for (let i = 0, il = nodeIds.length; i < il; i++) {

			pending.push(parser.getDependency('node', nodeIds[i]));

		}

		return Promise.all(pending).then(function (nodes) {

			for (let i = 0, il = nodes.length; i < il; i++) {

				scene.add(nodes[i]);

			}

			// Removes dangling associations, associations that reference a node that
			// didn't make it into the scene.
			const reduceAssociations = (node) => {

				const reducedAssociations = new Map();

				for (const [key, value] of parser.associations) {

					if (key instanceof Material || key instanceof Texture) {

						reducedAssociations.set(key, value);

					}

				}

				node.traverse((node) => {

					const mappings = parser.associations.get(node);

					if (mappings != null) {

						reducedAssociations.set(node, mappings);

					}

				});

				return reducedAssociations;

			};

			parser.associations = reduceAssociations(scene);

			return scene;

		});

	}

	_createAnimationTracks(node, inputAccessor, outputAccessor, sampler, target) {

		const tracks = [];

		const targetName = node.name ? node.name : node.uuid;
		const targetNames = [];

		if (PATH_PROPERTIES[target.path] === PATH_PROPERTIES.weights) {

			node.traverse(function (object) {

				if (object.morphTargetInfluences) {

					targetNames.push(object.name ? object.name : object.uuid);

				}

			});

		} else {

			targetNames.push(targetName);

		}

		let TypedKeyframeTrack;

		switch (PATH_PROPERTIES[target.path]) {

			case PATH_PROPERTIES.weights:

				TypedKeyframeTrack = NumberKeyframeTrack;
				break;

			case PATH_PROPERTIES.rotation:

				TypedKeyframeTrack = QuaternionKeyframeTrack;
				break;

			case PATH_PROPERTIES.position:
			case PATH_PROPERTIES.scale:

				TypedKeyframeTrack = VectorKeyframeTrack;
				break;

			default:

				switch (outputAccessor.itemSize) {

					case 1:
						TypedKeyframeTrack = NumberKeyframeTrack;
						break;
					case 2:
					case 3:
					default:
						TypedKeyframeTrack = VectorKeyframeTrack;
						break;

				}

				break;

		}

		const interpolation = sampler.interpolation !== undefined ? INTERPOLATION[sampler.interpolation] : InterpolateLinear;


		const outputArray = this._getArrayFromAccessor(outputAccessor);

		for (let j = 0, jl = targetNames.length; j < jl; j++) {

			const track = new TypedKeyframeTrack(
				targetNames[j] + '.' + PATH_PROPERTIES[target.path],
				inputAccessor.array,
				outputArray,
				interpolation
			);

			// Override interpolation with custom factory method.
			if (sampler.interpolation === 'CUBICSPLINE') {

				this._createCubicSplineTrackInterpolant(track);

			}

			tracks.push(track);

		}

		return tracks;

	}

	_getArrayFromAccessor(accessor) {

		let outputArray = accessor.array;

		if (accessor.normalized) {

			const scale = getNormalizedComponentScale(outputArray.constructor);
			const scaled = new Float32Array(outputArray.length);

			for (let j = 0, jl = outputArray.length; j < jl; j++) {

				scaled[j] = outputArray[j] * scale;

			}

			outputArray = scaled;

		}

		return outputArray;

	}

	_createCubicSplineTrackInterpolant(track) {

		track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline(result) {

			// A CUBICSPLINE keyframe in glTF has three output values for each input value,
			// representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize()
			// must be divided by three to get the interpolant's sampleSize argument.

			const interpolantType = (this instanceof QuaternionKeyframeTrack) ? GLTFCubicSplineQuaternionInterpolant : GLTFCubicSplineInterpolant;

			return new interpolantType(this.times, this.values, this.getValueSize() / 3, result);

		};

		// Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants.
		track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true;

	}

}

/**
 * @param {BufferGeometry} geometry
 * @param {GLTF.Primitive} primitiveDef
 * @param {GLTFParser} parser
 */
function computeBounds(geometry, primitiveDef, parser) {

	const attributes = primitiveDef.attributes;

	const box = new Box3();

	if (attributes.POSITION !== undefined) {

		const accessor = parser.json.accessors[attributes.POSITION];

		const min = accessor.min;
		const max = accessor.max;

		// glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement.

		if (min !== undefined && max !== undefined) {

			box.set(
				new Vector3(min[0], min[1], min[2]),
				new Vector3(max[0], max[1], max[2])
			);

			if (accessor.normalized) {

				const boxScale = getNormalizedComponentScale(WEBGL_COMPONENT_TYPES[accessor.componentType]);
				box.min.multiplyScalar(boxScale);
				box.max.multiplyScalar(boxScale);

			}

		} else {

			console.warn('THREE.GLTFLoader: Missing min/max properties for accessor POSITION.');

			return;

		}

	} else {

		return;

	}

	const targets = primitiveDef.targets;

	if (targets !== undefined) {

		const maxDisplacement = new Vector3();
		const vector = new Vector3();

		for (let i = 0, il = targets.length; i < il; i++) {

			const target = targets[i];

			if (target.POSITION !== undefined) {

				const accessor = parser.json.accessors[target.POSITION];
				const min = accessor.min;
				const max = accessor.max;

				// glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement.

				if (min !== undefined && max !== undefined) {

					// we need to get max of absolute components because target weight is [-1,1]
					vector.setX(Math.max(Math.abs(min[0]), Math.abs(max[0])));
					vector.setY(Math.max(Math.abs(min[1]), Math.abs(max[1])));
					vector.setZ(Math.max(Math.abs(min[2]), Math.abs(max[2])));


					if (accessor.normalized) {

						const boxScale = getNormalizedComponentScale(WEBGL_COMPONENT_TYPES[accessor.componentType]);
						vector.multiplyScalar(boxScale);

					}

					// Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative
					// to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets
					// are used to implement key-frame animations and as such only two are active at a time - this results in very large
					// boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size.
					maxDisplacement.max(vector);

				} else {

					console.warn('THREE.GLTFLoader: Missing min/max properties for accessor POSITION.');

				}

			}

		}

		// As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets.
		box.expandByVector(maxDisplacement);

	}

	geometry.boundingBox = box;

	const sphere = new Sphere();

	box.getCenter(sphere.center);
	sphere.radius = box.min.distanceTo(box.max) / 2;

	geometry.boundingSphere = sphere;

}

/**
 * @param {BufferGeometry} geometry
 * @param {GLTF.Primitive} primitiveDef
 * @param {GLTFParser} parser
 * @return {Promise<BufferGeometry>}
 */
function addPrimitiveAttributes(geometry, primitiveDef, parser) {

	const attributes = primitiveDef.attributes;

	const pending = [];

	function assignAttributeAccessor(accessorIndex, attributeName) {

		return parser.getDependency('accessor', accessorIndex)
			.then(function (accessor) {

				geometry.setAttribute(attributeName, accessor);

			});

	}

	for (const gltfAttributeName in attributes) {

		const threeAttributeName = ATTRIBUTES[gltfAttributeName] || gltfAttributeName.toLowerCase();

		// Skip attributes already provided by e.g. Draco extension.
		if (threeAttributeName in geometry.attributes) continue;

		pending.push(assignAttributeAccessor(attributes[gltfAttributeName], threeAttributeName));

	}

	if (primitiveDef.indices !== undefined && !geometry.index) {

		const accessor = parser.getDependency('accessor', primitiveDef.indices).then(function (accessor) {

			geometry.setIndex(accessor);

		});

		pending.push(accessor);

	}

	if (ColorManagement.workingColorSpace !== LinearSRGBColorSpace && 'COLOR_0' in attributes) {

		console.warn(`THREE.GLTFLoader: Converting vertex colors from "srgb-linear" to "${ColorManagement.workingColorSpace}" not supported.`);

	}

	assignExtrasToUserData(geometry, primitiveDef);

	computeBounds(geometry, primitiveDef, parser);

	return Promise.all(pending).then(function () {

		return primitiveDef.targets !== undefined
			? addMorphTargets(geometry, primitiveDef.targets, parser)
			: geometry;

	});

}

export { GLTFLoader };
